import SPI;
import Credential;
import Util;
import OSSUtil;
import OpenApiUtil;
import XML;
import String;
import Map;
import Array;
import EncodeUtil;
import SignatureUtil;
import Time;
import OSS_Util;

extends SPI;

type @default_signed_params = [ string ]
type @except_signed_params = [ string ]

init(){
  super();
  @default_signed_params = [
  'response-content-type', 'response-content-language',
  'response-cache-control', 'logging', 'response-content-encoding',
  'acl', 'uploadId', 'uploads', 'partNumber', 'group', 'link',
  'delete', 'website', 'location', 'objectInfo', 'objectMeta',
  'response-expires', 'response-content-disposition', 'cors', 'lifecycle',
  'restore', 'qos', 'referer', 'stat', 'bucketInfo', 'append', 'position', 'security-token',
  'live', 'comp', 'status', 'vod', 'startTime', 'endTime', 'x-oss-process',
  'symlink', 'callback', 'callback-var', 'tagging', 'encryption', 'versions',
  'versioning', 'versionId', 'policy', 'requestPayment', 'x-oss-traffic-limit', 'qosInfo', 'asyncFetch',
  'x-oss-request-payer', 'sequential', 'inventory', 'inventoryId', 'continuation-token', 'callback',
  'callback-var', 'worm', 'wormId', 'wormExtend', 'replication', 'replicationLocation',
  'replicationProgress', 'transferAcceleration', 'cname', 'metaQuery',
  'x-oss-ac-source-ip', 'x-oss-ac-subnet-mask', 'x-oss-ac-vpc-id', 'x-oss-ac-forward-allow',
  'resourceGroup', 'style', 'styleName', 'x-oss-async-process', 'rtc', 'accessPoint',
  'accessPointPolicy', "httpsConfig", 'regionsV2', 'publicAccessBlock', 'policyStatus',
  'redundancyTransition', 'redundancyType', 'redundancyProgress', 'dataAccelerator', 'verbose',
  'accessPointForObjectProcess', 'accessPointConfigForObjectProcess', 'accessPointPolicyForObjectProcess',
  'bucketArchiveDirectRead', 'responseHeader', 'userDefinedLogFieldsConfig', 'reservedcapacity',
  'requesterQosInfo', 'qosRequester', 'resourcePool', 'resourcePoolInfo', 'resourcePoolBuckets',
  'processConfiguration', 'img', 'asyncFetch', 'virtualBucket', 'copy', 'userRegion', 'partSize',
  'chunkSize', 'partUploadId', 'chunkNumber', 'userRegion', 'regionList', 'eventnotification',
  'cacheConfiguration', 'dfs', 'dfsadmin', 'dfssecurity'
  ];
  @except_signed_params = ['list-type', 'regions'];
}

async function modifyConfiguration(context: SPI.InterceptorContext, attributeMap: SPI.AttributeMap): void {
  var config = context.configuration;
  config.endpoint = getEndpoint(config.regionId, config.network, config.endpoint);
}

async function modifyRequest(context: SPI.InterceptorContext, attributeMap: SPI.AttributeMap): void {
  var request = context.request;
  var hostMap : map[string]string = {};
  if (!Util.isUnset(request.hostMap)) {
    hostMap = request.hostMap;
  }
  var bucketName = hostMap.bucket;
  if (Util.isUnset(bucketName)) {
    bucketName = '';
  }
  if (!Util.isUnset(request.headers['x-oss-meta-*'])) {
    var tmp : any = Util.parseJSON(request.headers['x-oss-meta-*']);
    var mapData : map[string]any = Util.assertAsMap(tmp);
    var metaData : map[string]string = Util.stringifyMapValue(mapData);
    var metaKeySet : [string] = Map.keySet(metaData);
    request.headers['x-oss-meta-*'] = null;
    for(var key : metaKeySet) {
      var newKey = `x-oss-meta-${key}`;
      request.headers[newKey] = metaData[key];
    }
  }
  var config = context.configuration;
  var regionId = config.regionId;
  if (Util.isUnset(regionId) || Util.empty(regionId)) {
    regionId = getRegionIdFromEndpoint(config.endpoint);
  }
  var credential : Credential = request.credential;
  var accessKeyId = credential.getAccessKeyId();
  var accessKeySecret = credential.getAccessKeySecret();
  var securityToken = credential.getSecurityToken();
  if (!Util.empty(securityToken)) {
      request.headers.x-oss-security-token = securityToken;
  }
  if (!Util.isUnset(request.body)) {
    if (String.equals(request.reqBodyType, 'xml')) {
      var reqBodyMap = Util.assertAsMap(request.body);
      // for python:
      // xml_str = OSS_UtilClient.to_xml(req_body_map)
      var xmlStr = XML.toXML(reqBodyMap);
      request.stream = xmlStr;
      request.headers.content-type = 'application/xml';
      request.headers.content-md5 = EncodeUtil.base64EncodeToString(SignatureUtil.MD5Sign(xmlStr));
    } else if (String.equals(request.reqBodyType, 'json')) {
      var reqBodyStr = Util.toJSONString(request.body);
      request.stream = reqBodyStr;
      request.headers.content-type = 'application/json; charset=utf-8';
    } else if (String.equals(request.reqBodyType, 'formData')) {
      var reqBodyForm = Util.assertAsMap(request.body);
      request.stream = OpenApiUtil.toForm(reqBodyForm);
      request.headers.content-type = 'application/x-www-form-urlencoded';
    } else if (String.equals(request.reqBodyType, 'binary')) {
      attributeMap.key = {
        crc = '',
        md5 = '',
      };
      request.stream = OSSUtil.inject(request.stream, attributeMap.key);
      request.headers.content-type = 'application/octet-stream';
    }
  }
  var host = getHost(config.endpointType, bucketName, config.endpoint, context);
  request.headers = {
    host = host,
    date = Util.getDateUTCString(),
    user-agent = request.userAgent,
    ...request.headers
  };

  var originPath = request.pathname;
  var originQuery = request.query;
  if (!Util.empty(originPath)) {
    var pathAndQueries : [string] = String.split(originPath, `?`, 2);
    request.pathname = pathAndQueries[0];
    if (Util.equalNumber(Array.size(pathAndQueries), 2)) {
      var pathQueries : [string] = String.split(pathAndQueries[1], '&', null);
      for (var sub : pathQueries) {
        var item : [string] = String.split(sub, '=', null);
        var queryKey : string = item[0];
        var queryValue : string = '';
        if (Util.equalNumber(Array.size(item), 2)) {
          queryValue = item[1];
        }
        if (Util.empty(originQuery[queryKey])) {
          request.query[queryKey] = queryValue;
        }
      }
    }
  }

  var signatureVersion = Util.defaultString(request.signatureVersion, 'v4');
  request.headers.authorization = getAuthorization(signatureVersion, bucketName, request.pathname, request.method, request.query, request.headers, accessKeyId, accessKeySecret, regionId);
}

async function modifyResponse(context: SPI.InterceptorContext, attributeMap: SPI.AttributeMap): void {
  var request = context.request;
  var response = context.response;
  var bodyStr : string = null;
  if (Util.is4xx(response.statusCode) || Util.is5xx(response.statusCode)) {
    bodyStr = Util.readAsString(response.body);
    if (!Util.empty(bodyStr)) {
      var respMap : map[string]any = XML.parseXml(bodyStr, null);
      var err : map[string]any = Util.assertAsMap(respMap.Error);
      throw {
        code = err.Code,
        message = err.Message,
        data = {
          statusCode = response.statusCode,
          requestId = err.RequestId,
          ecCode = err.EC,
          Recommend = err.RecommendDoc,
          hostId = err.HostId,
          AccessDeniedDetail = err.AccessDeniedDetail,
        }
      };
    } else {
      var headers : map[string]string = response.headers;
      var requestId = headers.x-oss-request-id;
      var ecCode = headers.x-oss-ec-code;
      throw {
        code = response.statusCode,
        message = null,
        data = {
          statusCode = response.statusCode,
          requestId = `${requestId}`,
          ecCode = ecCode,
        }
      };
    }
    
  }
  var ctx : map[string]string = attributeMap.key;
  if (!Util.isUnset(ctx)) {
    if (!Util.isUnset(ctx.crc)
            && !Util.isUnset(response.headers.x-oss-hash-crc64ecma)
            && !String.equals(ctx.crc, response.headers.x-oss-hash-crc64ecma)) {
      throw {
        code = 'CrcNotMatched',
        data = {
          clientCrc = ctx.crc,
          serverCrc = response.headers.x-oss-hash-crc64ecma,
        },
      };
    }
    if (!Util.isUnset(ctx.md5)
            && !Util.isUnset(response.headers.content-md5)
            && !String.equals(ctx.md5, response.headers.content-md5)) {
      throw {
        code = 'MD5NotMatched',
        data = {
          clientMD5 = ctx.md5,
          serverMD5 = response.headers.content-md5,
        },
      };
    }
  }
  if (!Util.isUnset(response.body)) {
    if (Util.equalNumber(response.statusCode, 204)) {
      Util.readAsString(response.body);
    } else if (String.equals(request.bodyType, 'xml')) {
      bodyStr = Util.readAsString(response.body);
      response.deserializedBody = bodyStr;
      if (!Util.empty(bodyStr)) {
        var result : any = OSS_Util.parseXml(bodyStr, request.action);
        // for no util language
        // var result : any = XML.parseXml(bodyStr, null);
        try {
          response.deserializedBody = Util.assertAsMap(result);
        } catch (error) {
          response.deserializedBody = result;
        }
      }
    } else if (Util.equalString(request.bodyType, 'binary')) {
      response.deserializedBody = response.body;
    } else if (Util.equalString(request.bodyType, 'byte')) {
      var byt = Util.readAsBytes(response.body);
      response.deserializedBody = byt;
    } else if (Util.equalString(request.bodyType, 'string')) {
      response.deserializedBody = Util.readAsString(response.body);
    } else if (Util.equalString(request.bodyType, 'json')) {
      var obj = Util.readAsJSON(response.body);
      var res = Util.assertAsMap(obj);
      response.deserializedBody = res;
    } else if (Util.equalString(request.bodyType, 'array')) {
      response.deserializedBody = Util.readAsJSON(response.body);
    } else {
      response.deserializedBody = Util.readAsString(response.body);
    }
  }
}

async function getRegionIdFromEndpoint(endpoint: string) : string {
  if (!Util.empty(endpoint)) {
    var idx: integer = -1;
    if (String.hasPrefix(endpoint, 'oss-') && String.hasSuffix(endpoint, '.aliyuncs.com')) {
      idx = String.index(endpoint, '.aliyuncs.com');
      return String.subString(endpoint, 4, idx);
    }
    if (String.hasSuffix(endpoint, '.mgw.aliyuncs.com')) {
      idx = String.index(endpoint, '.mgw.aliyuncs.com');
      return String.subString(endpoint, 0, idx);
    }
    if (String.hasSuffix(endpoint, '.mgw-internal.aliyuncs.com')) {
      idx = String.index(endpoint, '.mgw-internal.aliyuncs.com');
      return String.subString(endpoint, 0, idx);
    }
    if (String.hasSuffix(endpoint, '-internal.oss-data-acc.aliyuncs.com')) {
      idx = String.index(endpoint, '-internal.oss-data-acc.aliyuncs.com');
      return String.subString(endpoint, 0, idx);
    }
    if (String.hasSuffix(endpoint, '.oss-dls.aliyuncs.com')) {
      idx = String.index(endpoint, '.oss-dls.aliyuncs.com');
      return String.subString(endpoint, 0, idx);
    }
  }
  return 'cn-hangzhou';
}

async function getEndpoint(regionId: string, network: string, endpoint: string) : string{
  if (!Util.empty(endpoint)) {
    return endpoint;
  }
  if (Util.empty(regionId)) {
    regionId = 'cn-hangzhou';
  }
  if (!Util.empty(network)) {
    if (String.contains(network, 'internal')) {
      return `oss-${regionId}-internal.aliyuncs.com`;
    } else if (String.contains(network, 'ipv6')) {
      return `${regionId}oss.aliyuncs.com`;
    } else if (String.contains(network, 'accelerate')) {
      return `oss-${network}.aliyuncs.com`;
    }
  }
  return `oss-${regionId}.aliyuncs.com`;
}

async function getHost(endpointType: string, bucketName: string, endpoint: string, context: SPI.InterceptorContext): string {
  if (String.contains(endpoint,'.mgw.aliyuncs.com') && !Util.isUnset(context.request.hostMap.userid)) {
    return `${context.request.hostMap.userid}.${endpoint}`;
  }
  if (String.contains(endpoint,'.mgw-internal.aliyuncs.com') && !Util.isUnset(context.request.hostMap.userid)) {
    return `${context.request.hostMap.userid}.${endpoint}`;
  }
  if (Util.empty(bucketName)) {
    return endpoint;
  }
  var host : string = `${bucketName}.${endpoint}`;
  if (!Util.empty(endpointType)) {
    if (String.equals(endpointType, 'ip')) {
      host = `${endpoint}/${bucketName}`;
    } else if (String.equals(endpointType, 'cname')) {
      host = endpoint;
    }
  }
  return host;
}

async function getAuthorization(signatureVersion: string, bucketName: string, pathname: string, method: string, query: map[string]string, headers: map[string]string, ak: string, secret: string, regionId: string): string {
  var sign : string = '';
  if (!Util.isUnset(signatureVersion)) {
    if (String.equals(signatureVersion, 'v1')) {
      sign = getSignatureV1(bucketName, pathname, method, query, headers, secret);
      return `OSS ${ak}:${sign}`;
    }
    if (String.equals(signatureVersion, 'v2')) {
      sign = getSignatureV2(bucketName, pathname, method, query, headers, secret);
      return `OSS2 AccessKeyId:${ak},Signature:${sign}`;
    }
  }

  var dateTime = OpenApiUtil.getTimestamp();
  dateTime = String.replace(dateTime, '-', '', null);
  dateTime = String.replace(dateTime, ':', '', null);
  headers.x-oss-date = dateTime;
  headers.x-oss-content-sha256 = 'UNSIGNED-PAYLOAD';

  var onlyDate : string = String.subString(dateTime, 0, 8);
  var cred : string = `${ak}/${onlyDate}/${regionId}/oss/aliyun_v4_request`;
  sign = getSignatureV4(bucketName, pathname, method, query, headers, onlyDate, regionId, secret);

  return `OSS4-HMAC-SHA256 Credential=${cred}, Signature=${sign}`;
}

async function getSignKey(secret: string, onlyDate: string, regionId: string): bytes {
  var temp = `aliyun_v4${secret}`;
  var res = SignatureUtil.HmacSHA256Sign(onlyDate, temp);
  res = SignatureUtil.HmacSHA256SignByBytes(regionId, res);
  res = SignatureUtil.HmacSHA256SignByBytes('oss', res);
  res = SignatureUtil.HmacSHA256SignByBytes('aliyun_v4_request', res);
  return res;
}

async function getSignatureV4(bucketName: string, pathname: string, method: string, query: map[string]string, headers: map[string]string, onlyDate: string, regionId: string, secret: string): string {
  var signingkey : bytes = getSignKey(secret, onlyDate, regionId);

  var canonicalizedUri : string = pathname;
  if (!Util.empty(pathname)) {
    if (!Util.empty(bucketName)) {
       canonicalizedUri = `/${bucketName}${canonicalizedUri}`;
    }
  } else {
    if (!Util.empty(bucketName)) {
      canonicalizedUri = `/${bucketName}/`;
    } else {
      canonicalizedUri = "/";
    }
  }

  // for java:
  // String suffix = (!canonicalizedUri.equals("/") && canonicalizedUri.endsWith("/"))? "/" : "";
  // canonicalizedUri = com.aliyun.openapiutil.Client.getEncodePath(canonicalizedUri) + suffix;
  canonicalizedUri = OpenApiUtil.getEncodePath(canonicalizedUri);

  var queryMap : map[string]string = {};
  for (var queryKey : Map.keySet(query)) {
    var queryValue : string = null;
    if (!Util.empty(query[queryKey])) {
      queryValue = EncodeUtil.percentEncode(query[queryKey]);
      queryValue = String.replace(queryValue, '+', '%20', null);
    }
    queryKey = EncodeUtil.percentEncode(queryKey);
    queryKey = String.replace(queryKey, '+', '%20', null);
    // for go : queryMap[tea.StringValue(queryKey)] = queryValue
    queryMap[queryKey] = queryValue;
  }
  var canonicalizedQueryString = buildCanonicalizedQueryStringV4(queryMap);
  var canonicalizedHeaders = buildCanonicalizedHeadersV4(headers);
  var payload = 'UNSIGNED-PAYLOAD';
  var canonicalRequest = `${method}\n${canonicalizedUri}\n${canonicalizedQueryString}\n${canonicalizedHeaders}\n\n${payload}`;
  var hex = EncodeUtil.hexEncode(EncodeUtil.hash(Util.toBytes(canonicalRequest), 'ACS4-HMAC-SHA256'));
  var scope :string = `${onlyDate}/${regionId}/oss/aliyun_v4_request`;
  var stringToSign = `OSS4-HMAC-SHA256\n${headers.x-oss-date}\n${scope}\n${hex}`;
  var signature = SignatureUtil.HmacSHA256SignByBytes(stringToSign, signingkey);
  return EncodeUtil.hexEncode(signature);
}

async function buildCanonicalizedQueryStringV4(queryMap: map[string]string): string {
  var canonicalizedQueryString : string = '';
  if (!Util.isUnset(queryMap)) {
    var queryArray : [string] = Map.keySet(queryMap);
    var sortedQueryArray = Array.ascSort(queryArray);
    var separator : string = '';
    for(var key : sortedQueryArray) {
      canonicalizedQueryString = `${canonicalizedQueryString}${separator}${key}`;
      if (!Util.empty(queryMap[key])) {
        canonicalizedQueryString = `${canonicalizedQueryString}=${queryMap[key]}`;
      }
      separator = '&';
    }
  }
  return canonicalizedQueryString;
}

async function buildCanonicalizedHeadersV4(headers: map[string]string): string {
  var canonicalizedHeaders : string = '';
  var headersArray : [string] = Map.keySet(headers);
  var sortedHeadersArray = Array.ascSort(headersArray);
  for(var key : sortedHeadersArray) {
    var lowerKey = String.toLower(key);
    if (String.hasPrefix(lowerKey, 'x-oss-')
            || String.equals(lowerKey, 'content-type') || String.equals(lowerKey, 'content-md5')) {
      canonicalizedHeaders = `${canonicalizedHeaders}${key}:${String.trim(headers[key])}\n`;
    }
  }
  return canonicalizedHeaders;
}

async function getSignatureV1(bucketName: string, pathname: string, method: string, query: map[string]string, headers: map[string]string, secret: string): string {
  var resource : string = '';
  var stringToSign : string = '';
  if (!Util.empty(bucketName)) {
    resource = `/${bucketName}`;
  }
  resource = `${resource}${pathname}`;
  var canonicalizedResource = buildCanonicalizedResource(resource, query);
  var canonicalizedHeaders = buildCanonicalizedHeaders(headers);
  stringToSign = `${method}\n${canonicalizedHeaders}${canonicalizedResource}`;
  return EncodeUtil.base64EncodeToString(SignatureUtil.HmacSHA1Sign(stringToSign, secret));
}

async function buildCanonicalizedResource(pathname: string, query: map[string]string): string {
  var canonicalizedResource : string = pathname;
  var queryKeys : [string] = Map.keySet(query);
  var sortedParams = Array.ascSort(queryKeys);
  var separator : string = '?';
  for(var paramName : sortedParams) {
    if (Array.contains(@default_signed_params, paramName) || String.hasPrefix(paramName, 'x-oss-')) {
      canonicalizedResource = `${canonicalizedResource}${separator}${paramName}`;
      if (!Util.empty(query[paramName])) {
        canonicalizedResource = `${canonicalizedResource}=${query[paramName]}`;
      }
      separator = '&';
    }
  }
  return canonicalizedResource;
}

async function buildCanonicalizedHeaders(headers: map[string]string): string {
  var canonicalizedHeaders : string = '';
  var contentType = headers.content-type;
  if (Util.isUnset(contentType)) {
    contentType = '';
  }
  var contentMd5 = headers.content-md5;
  if (Util.isUnset(contentMd5)) {
    contentMd5 = '';
  }
  canonicalizedHeaders = `${canonicalizedHeaders}${contentMd5}\n${contentType}\n${headers.date}\n`;
  var keys = Map.keySet(headers);
  var sortedHeaders = Array.ascSort(keys);
  for(var header : sortedHeaders) {
    if (String.contains(String.toLower(header), 'x-oss-') && !Util.isUnset(headers[header])) {
      canonicalizedHeaders = `${canonicalizedHeaders}${header}:${headers[header]}\n`;
    }
  }
  return canonicalizedHeaders;
}

async function getSignatureV2(bucketName: string, pathname: string, method: string, query: map[string]string, headers: map[string]string, secret: string): string {
  return '';
}